Imports System.IO
Imports System.Net
Imports System.Net.Sockets
Imports System.Security.Cryptography.X509Certificates
Imports System.Security.Authentication
Imports System.Net.Security
Imports System.Timers

Public Class TCPClientSocketClient  ' The class that instatiates the client objects.
    '
    'Keep track of client information
    '
    Private _client As TcpClient
    Private _clientIP As String

    '
    'Keep track of packet and accumulated data for this specific socket connection
    '
    Private input_packet_data() As Byte                     'input buffer handles only one packet transmission at a time
    Private output_buffer_data() As Byte                    'output buffer may have one or many packets to dispatch to external executible
    Private string_required_to_commit As String = String.Empty
    Private exclude_required_string As Boolean = False
    '
    'SSL specific information including the certificate and current SSL stream
    '
    Private UsingSSL As Boolean = False                     'boolean that tells if we are using SSL
    Private SSLCertificate As X509Certificate = Nothing     'The actual server SSL certificate 
    Private SSLCommunicationsStream As SslStream = Nothing  'Global (for this socket) transmission stream for SSL communications

    '
    'Miscellaneous stuff to keep track of...
    '
    Private TransmissionTimer As Timer = Nothing            'Timer to determine if we are ready to purge/flush the buffer to the external executible.  We can't wait forever!!!  We have stuff to do!!!
    Private PacketName As String = String.Empty             'Essentially just the IP and CLIENT source port being used
    Private PacketSerialNum As Integer = 1                  'A way to keep track of multiple transmissions per socket.  This comes into play if client does not close socket.
    Private MillisecondDelayBeforePacketSend As Integer = 500 'Default to half of a second
    Public whoDisconnected As String
    Public whoConnected As String

    '
    'Published events
    '
    Public Shared Event NewIncomingConnection(ByVal whoConnected As String)                                                             'event to trigger connection
    Public Shared Event ExistingConnectionClosed(ByVal whoDisconnected As String)                                                       'event to trigger disconnection
    Public Shared Event NewDataArrived(ByVal ClientIP As String, ByVal datain As Byte(), ByVal PacketSerialNumber As Integer)           'event triggers on new data input

    '
    'Static collection to keep track of all socket connections!!!  It's contents are maintained like a global for all client connections
    '
    Public Shared AllTCPClientSockets As New Hashtable 'Keeps track for all active connections - IP:port

    '
    'Returns just a specified number of bytes from an array.  Essentially truncates remaining bytes off of the end...
    '
    Private Function LeftSubArray(ByVal x() As Byte, ByVal HowManyBytes As Integer) As Byte()
        ReDim Preserve x(HowManyBytes - 1)
        Return x
    End Function

    '
    'Returns only the end of an array starting at a position within the source array
    '
    Private Function RightSubArray(ByVal x() As Byte, ByVal PositionInArray As Integer) As Byte()
        Dim y((x.Length - PositionInArray) - 1) As Byte
        Dim i As Integer
        Dim iIndex As Integer = 0
        For i = PositionInArray To x.Length - 1
            y(iIndex) = x(i)
            iIndex += 1
        Next
        Return y
    End Function

    '
    'Concatenates two byte arrays and returns the result
    '
    Private Function ArrayConcat(ByVal x() As Byte, ByVal y() As Byte) As Byte()
        Dim tempStr As String = ""
        Try
            If x.Length > 0 Then
                Dim xL As Integer = x.Length
                ReDim Preserve x(xL + y.Length - 1)
                y.CopyTo(x, xL)
                Return x
            Else
                Return y
            End If
        Catch ex As Exception
            Return y
        End Try
    End Function

    '
    'Enables the transmission wait timer.  If the timer fires, there has been no incoming data for the last 500ms.  This signifies a pause so we can process what we have.
    '
    Private Sub StartTimer()

        If Not (TransmissionTimer Is Nothing) Then
            TransmissionTimer.Enabled = False
            TransmissionTimer = Nothing
        End If

        TransmissionTimer = New Timer(MillisecondDelayBeforePacketSend)
        AddHandler TransmissionTimer.Elapsed, New ElapsedEventHandler(AddressOf TimerHandler)
        TransmissionTimer.Enabled = True
    End Sub

    '
    'Fires when there is a pause in the transmission.  This allows us to process the current buffers...
    '
    Private Sub TimerHandler(ByVal sender As Object, ByVal e As ElapsedEventArgs)
        If Not (TransmissionTimer Is Nothing) Then
            SyncLock TransmissionTimer
                Try
                    TransmissionTimer.Enabled = False
                    TransmissionTimer = Nothing
                Catch ex As Exception
                    TransmissionTimer = Nothing
                End Try
            End SyncLock
        End If
        Call OutputData()
    End Sub
    '  Write data back to client process
    Sub OutputData()
        SyncLock output_buffer_data
            If output_buffer_data.Length > 0 Then
                If string_required_to_commit <> String.Empty Then
                    If ByteArrayToStr(output_buffer_data).Contains(string_required_to_commit.Replace("\r", vbCr).Replace("\n", vbLf)) Then
                        If exclude_required_string Then
                            output_buffer_data = StrToByteArray(ByteArrayToStr(output_buffer_data).Replace(string_required_to_commit.Replace("\r", vbCr).Replace("\n", vbLf), String.Empty))
                        End If
                        ProcessMessage(output_buffer_data, PacketName)
                        If PacketSerialNum < Integer.MaxValue Then
                            PacketSerialNum += 1
                        Else
                            PacketSerialNum = 1
                        End If
                        ReDim output_buffer_data(-1)
                    End If
                Else
                    ProcessMessage(output_buffer_data, PacketName)
                    If PacketSerialNum < Integer.MaxValue Then
                        PacketSerialNum += 1
                    Else
                        PacketSerialNum = 1
                    End If
                    ReDim output_buffer_data(-1)
                End If
            End If
        End SyncLock
    End Sub
    '
    'Listener has invoked us because a new incoming connection has been detected
    '
    Public Sub New(ByVal client As TcpClient, ByVal SSL As Boolean, ByRef ServerCertificate As X509Certificate, TimerDelay As Integer, ReqdStringToCommit As String, ExcludeString As Boolean)
        SyncLock GetType(TCPClientSocketClient)

            _client = client
            _clientIP = client.Client.RemoteEndPoint.ToString

            string_required_to_commit = ReqdStringToCommit
            exclude_required_string = ExcludeString

            MillisecondDelayBeforePacketSend = TimerDelay

            AllTCPClientSockets.Add(_clientIP, Me) ' Update of the Hash Table. ' SyncLock may be needed here
            Me.whoConnected = _clientIP
            Try
                RaiseEvent NewIncomingConnection(whoConnected) 'Tell parent we have a new connection

                ReDim input_packet_data(_client.ReceiveBufferSize - 1) 'Socket's Buffer reading starts here.
                Try
                    If SSL And (Not (ServerCertificate Is Nothing)) Then
                        UsingSSL = True
                        SSLCertificate = ServerCertificate
                        SSLCommunicationsStream = New SslStream(client.GetStream(), False)
                        SSLCommunicationsStream.AuthenticateAsServer(SSLCertificate, False, SslProtocols.Tls, False)
                        SSLCommunicationsStream.ReadTimeout = 5000
                        SSLCommunicationsStream.WriteTimeout = 5000
                        If SSLCommunicationsStream.IsAuthenticated And SSLCommunicationsStream.IsEncrypted Then
                            SSLCommunicationsStream.BeginRead(input_packet_data, 0, CInt(_client.ReceiveBufferSize), AddressOf ReceiveSSLMessage, Nothing)
                        Else
                            _client.Close()

                            AllTCPClientSockets.Remove(_clientIP)
                            Me.whoDisconnected = _clientIP

                            _clientIP = Nothing

                            RaiseEvent ExistingConnectionClosed(whoDisconnected)
                        End If
                    Else
                        _client.GetStream.ReadTimeout = 5000
                        _client.GetStream.BeginRead(input_packet_data, 0, CInt(_client.ReceiveBufferSize), AddressOf ReceiveMessage, Nothing)
                    End If

                Catch ex As Exception
                    AllTCPClientSockets.Remove(_clientIP)
                End Try
            Catch ex2 As Exception
                AllTCPClientSockets.Remove(_clientIP)
            End Try

        End SyncLock
    End Sub

    '
    'Disconnect current client socket connection
    '
    Public Sub Disconnect()
        Try
            _client.Close()

            AllTCPClientSockets.Remove(_clientIP)
            Me.whoDisconnected = _clientIP

            _clientIP = Nothing

            RaiseEvent ExistingConnectionClosed(whoDisconnected)

        Catch ex As Exception
            Try
                AllTCPClientSockets.Remove(_clientIP)
            Catch ex2 As Exception

            End Try
            Me.whoDisconnected = _clientIP
            _clientIP = Nothing
            RaiseEvent ExistingConnectionClosed(whoDisconnected)
        End Try

    End Sub

    '
    'Handles new SSL transmission
    '
    Private Sub ReceiveSSLMessage(ByVal ar As IAsyncResult)

        Dim bytesRead As Integer
        Dim RcvSocket As Socket = _client.Client

        Try
            If Not (TransmissionTimer Is Nothing) Then
                TransmissionTimer.Enabled = False
            End If
            If PacketName = String.Empty Then
                PacketName = CType(RcvSocket.RemoteEndPoint, IPEndPoint).Address.ToString() & ":" & CType(RcvSocket.RemoteEndPoint, IPEndPoint).Port.ToString()
            End If

            If Not (RcvSocket.Connected) Then
                _client.Close()

                AllTCPClientSockets.Remove(PacketName) ' Update of the Hash Table.
                Me.whoDisconnected = PacketName

                _clientIP = Nothing

                RaiseEvent ExistingConnectionClosed(whoDisconnected)

                Exit Sub
            End If
            SyncLock SSLCommunicationsStream
                bytesRead = SSLCommunicationsStream.EndRead(ar)
            End SyncLock

            If bytesRead < 1 Then ' Remote Client closed connection 

                _client.Close() ' So this command is needed.

                AllTCPClientSockets.Remove(PacketName) ' Update of the Hash Table. 
                Me.whoDisconnected = PacketName

                _clientIP = Nothing

                RaiseEvent ExistingConnectionClosed(whoDisconnected)

                Exit Sub
            Else
                Try
                    input_packet_data = LeftSubArray(input_packet_data, bytesRead)
                    If output_buffer_data Is Nothing Then
                        ReDim output_buffer_data(-1)
                    End If
                    SyncLock output_buffer_data
                        output_buffer_data = ArrayConcat(output_buffer_data, input_packet_data)
                    End SyncLock
                    ReDim input_packet_data(_client.ReceiveBufferSize - 1)
                    If MillisecondDelayBeforePacketSend <= 0 Then
                        Call OutputData()
                    End If
                    SyncLock SSLCommunicationsStream  'Read next packet of data from remote client
                        SSLCommunicationsStream.BeginRead(input_packet_data, 0, CInt(_client.ReceiveBufferSize), AddressOf ReceiveSSLMessage, Nothing)
                    End SyncLock
                    If MillisecondDelayBeforePacketSend > 0 Then
                        StartTimer()
                    End If
                Catch ex As Exception

                End Try
            End If

        Catch ex As Exception
            If PacketName <> String.Empty Then
                AllTCPClientSockets.Remove(PacketName) ' Update of the Hash Table. 
                Me.whoDisconnected = PacketName
                RaiseEvent ExistingConnectionClosed(whoDisconnected)
            End If
        End Try

    End Sub

    Private Sub ReceiveMessage(ByVal ar As IAsyncResult)

        Dim bytesRead As Integer
        Dim RcvSocket As Socket
        Dim ClientStream As System.Net.Sockets.NetworkStream
        Try
            If Not (TransmissionTimer Is Nothing) Then
                TransmissionTimer.Enabled = False
            End If
            RcvSocket = _client.Client
            ClientStream = _client.GetStream
            If PacketName = String.Empty Then
                PacketName = CType(RcvSocket.RemoteEndPoint, IPEndPoint).Address.ToString() & ":" & CType(RcvSocket.RemoteEndPoint, IPEndPoint).Port.ToString()
            End If
            If Not (RcvSocket.Connected) Then
                _client.Close()

                AllTCPClientSockets.Remove(PacketName) ' Update of the Hash Table. 
                Me.whoDisconnected = PacketName

                _clientIP = Nothing

                RaiseEvent ExistingConnectionClosed(whoDisconnected)

                Exit Sub
            End If
            SyncLock ClientStream
                bytesRead = ClientStream.EndRead(ar)
            End SyncLock

            If bytesRead < 1 Then ' Remote Client closed connection

                _client.Close() ' So this command is needed.

                AllTCPClientSockets.Remove(PacketName) ' Update of the Hash Table. 
                Me.whoDisconnected = PacketName

                _clientIP = Nothing

                RaiseEvent ExistingConnectionClosed(whoDisconnected)

                Exit Sub

            Else

                Try
                    input_packet_data = LeftSubArray(input_packet_data, bytesRead)
                    If output_buffer_data Is Nothing Then
                        ReDim output_buffer_data(-1)
                    End If
                    SyncLock output_buffer_data
                        output_buffer_data = ArrayConcat(output_buffer_data, input_packet_data)
                    End SyncLock
                    ReDim input_packet_data(_client.ReceiveBufferSize - 1)
                    If MillisecondDelayBeforePacketSend <= 0 Then
                        Call OutputData()
                    End If
                    SyncLock ClientStream  'Read next packet of data from remote client
                        ClientStream.BeginRead(input_packet_data, 0, CInt(_client.ReceiveBufferSize), AddressOf ReceiveMessage, Nothing)
                    End SyncLock
                    If MillisecondDelayBeforePacketSend > 0 Then
                        StartTimer()
                    End If
                Catch ex As Exception

                End Try

            End If

        Catch ex As Exception
            If PacketName <> String.Empty Then
                AllTCPClientSockets.Remove(PacketName) ' Update of the Hash Table. 
                Me.whoDisconnected = PacketName
                RaiseEvent ExistingConnectionClosed(whoDisconnected)
            End If
        End Try

    End Sub
    Private Function StrToByteArray(str As String) As Byte()
        Dim encoding As New System.Text.UTF8Encoding()
        Return encoding.GetBytes(str)
    End Function
    Private Function ByteArrayToStr(Bytes As Byte()) As String
        ByteArrayToStr = System.Text.Encoding.ASCII.GetString(Bytes)
    End Function

    Public Function SendMessage(ByVal message As Byte()) As Boolean

        ' Here you can pass any message you want to send to the remote client.

        Try
            Dim ns As System.Net.Sockets.NetworkStream
            Dim sslns As SslStream
            If UsingSSL Then
                sslns = SSLCommunicationsStream
                SyncLock sslns
                    sslns.Write(message, 0, message.Length)
                    sslns.Flush()
                End SyncLock
            Else
                ns = _client.GetStream
                SyncLock ns
                    ns.Write(message, 0, message.Length)
                    ns.Flush()
                End SyncLock
            End If

            Return True
        Catch ex As Exception
            Return False
        End Try

    End Function

    Private Function IsFileLocked(filename As String) As Boolean
        Dim stream As FileStream = Nothing
        Dim file As New FileInfo(filename)
        Try
            If System.IO.File.Exists(filename) Then
                stream = file.Open(FileMode.Open, FileAccess.ReadWrite, FileShare.None)
            End If
        Catch generatedExceptionName As IOException
            'the file is unavailable because it is: 
            'still being written to 
            'or being processed by another thread 
            'or does not exist (has already been processed) 
            Return True
        Finally
            If stream IsNot Nothing Then
                stream.Close()
            End If
        End Try

        'file is not locked 
        Return False
    End Function
    Private Function AppendToFile(msg As String, filepath As String) As Boolean
        Dim sw As System.IO.FileStream = Nothing
        Try
            AppendToFile = False
            If Not IsFileLocked(filepath) Then
                If System.IO.File.Exists(filepath) Then
                    File.Delete(filepath)
                End If

                sw = New FileStream(filepath, FileMode.Append, FileAccess.ReadWrite)
                sw.Write(StrToByteArray(msg), 0, StrToByteArray(msg).Length)
                sw.Close()
                AppendToFile = True
            End If
        Catch
            AppendToFile = False
        End Try
    End Function
    Public Function StreamFile(ByVal FileName As String) As Boolean
        Try
            StreamFile = False
            Dim ns As System.Net.Sockets.NetworkStream
            Dim InputStream As FileStream = New FileStream(FileName.TrimEnd, FileMode.Open, FileAccess.Read)
            Dim sslns As SslStream
            If UsingSSL Then
                sslns = SSLCommunicationsStream
                SyncLock sslns
                    Try
                        Const MaxBufferLen As Integer = 4095
                        Dim buffer As Byte() = New Byte(MaxBufferLen) {}
                        Dim bytesRead As Integer = 0
                        ' Do whatever with buffer
                        bytesRead = InputStream.Read(buffer, 0, buffer.Length)

                        While bytesRead > 0
                            sslns.Write(buffer, 0, bytesRead)
                            bytesRead = InputStream.Read(buffer, 0, buffer.Length)
                        End While
                        sslns.Flush()
                        InputStream.Close()
                        System.IO.File.Delete(FileName)
                        StreamFile = True
                    Catch ex As Exception
                        StreamFile = False
                        AppendToFile(vbCrLf & vbCrLf & "SSL StreamFile ERROR:" & ex.Message, FileName)
                    End Try
                End SyncLock
            Else
                ns = _client.GetStream
                SyncLock ns
                    Try
                        Const MaxBufferLen As Integer = 4095
                        Dim buffer As Byte() = New Byte(MaxBufferLen) {}
                        Dim bytesRead As Integer = 0
                        ' Do whatever with buffer
                        bytesRead = InputStream.Read(buffer, 0, buffer.Length)
                        While bytesRead > 0
                            ns.Write(buffer, 0, bytesRead)
                            bytesRead = InputStream.Read(buffer, 0, buffer.Length)
                        End While
                        ns.Flush()
                        InputStream.Close()
                        StreamFile = True
                        System.IO.File.Delete(FileName)
                    Catch ex As Exception
                        StreamFile = False
                        AppendToFile(vbCrLf & vbCrLf & "StreamFile ERROR:" & ex.Message, FileName)
                    End Try

                End SyncLock
            End If
        Catch ex As Exception
            AppendToFile(vbCrLf & vbCrLf & "ERROR:" & ex.Message, FileName)
            StreamFile = False
        End Try
    End Function

    Public Sub ProcessMessage(ByVal str As Byte(), ByVal theIP As String)
        RaiseEvent NewDataArrived(theIP, str, PacketSerialNum)
    End Sub


End Class

